from typing import List, Optional

import uvicorn
from fastapi import FastAPI, Depends, HTTPException, Path
from fastapi.openapi.docs import get_swagger_ui_html
from pydantic import BaseModel, Field
from sqlalchemy import create_engine, String, Integer, select, asc, update
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
from starlette.staticfiles import StaticFiles



# 创建 FastAPI 应用实例，并把默认的/docs请求禁用
# debug表示是否开启调试模式，默认是False
app = FastAPI(docs_url=None,debug=True)
app.mount("/static", StaticFiles(directory="../static"), name="static")    # 静态文件

@app.get("/docs", include_in_schema=False)
def custom_swagger_ui():
    return get_swagger_ui_html(
        openapi_url = "/openapi.json",
        title = "Swagger UI for FastAPI",
        swagger_js_url="/static/swagger-ui-bundle.min.js",
        swagger_css_url="/static/swagger-ui.min.css"
    )

DATABASE_URL = r"mysql+pymysql://root:A7#mZ9!pL3$q@localhost:3306/test"

# 数据映射 Base基类
class Base(DeclarativeBase):
    pass

# 数据库引擎
engine = create_engine(url=DATABASE_URL,echo=True)

# 定义模型类
class StudentEntity(Base):
    __tablename__ = "students"

    # primary_key=True 主键
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    # unique=True 唯一; nullable=False 不可以为空
    name: Mapped[str] = mapped_column(String(20), unique=True, nullable=False)
    gender: Mapped[str] = mapped_column(String(10), nullable=False)

# 根据映射类在数据库创建出数据表students(如果表不存在)
Base.metadata.create_all(engine)

# 创建数据库会话Session
Session = sessionmaker(bind=engine)

# 定义API的输入输出Schema类
class StudentBase(BaseModel):
    name: str = Field(..., examples=["学生姓名"])
    gender: str = Field(..., examples=["学生性别"])

class StudentCreate(StudentBase):
    ...

class StudentOut(StudentBase):
    id: int = Field(..., examples=[1])

def get_db_session():
    db_session = Session()
    try:
        # 这里使用yield是为了在请求开始运行到这里提供资源，并在请求结束后执行yield后的代码释放资源
        yield db_session
    finally:
        db_session.close()

@app.get("/students", response_model=List[StudentOut], tags=["学生 - 查询"])
async def get_students(db_session: Session = Depends(get_db_session)):
    # 查询所有学生并根据name升序排序
    query = select(StudentEntity).order_by(asc(StudentEntity.name))
    return db_session.execute(query).scalars().all()


@app.post("/students", response_model=StudentOut, tags=["学生 - 创建"])
async def create_student(student: StudentCreate, db_session: Session = Depends(get_db_session)):
    # 检查提交的数据
    query = select(StudentEntity).where(StudentEntity.name == student.name)
    record = db_session.execute(query).scalars().first()
    if record:
        raise HTTPException(status_code=400, detail=f"Student中{student.name}已存在!")

    student_entity = StudentEntity(name=student.name, gender=student.gender)
    db_session.add(student_entity)
    # SQLAlchemy是默认不自动提交的，这里需要手动进行提交
    db_session.commit()
    return student_entity

# 这里单独定义一个StudentUpdate是为了与StudentCreate分开来，当前两者是一样的未来如果两者有不同的诉求需要修改时不会有干扰
class StudentUpdate(StudentBase):
    ...


def set_attrs(obj, data:dict):
    """
    修改属性
    :param obj: 要修改哪一个属性中的数据
    :param data: 要修改的是哪些数据 是一个dict
    :return:
    """
    for key, value in data.items():
        # 修改哪一个对象obj中的指定属性key的值为value
        if hasattr(obj, key) and getattr(obj, key) is not None:
            setattr(obj, key, value)


@app.put("/students/{id}", response_model=StudentOut, tags=["学生 - 修改"])
# 第一个设置了设置为Path变量了对于Python来说相当于有了默认值，为保证后面的参数不报错，在函数参数中最开始添加一个*
async def update_student(*,id: int=Path(...), student: StudentUpdate, db_session: Session = Depends(get_db_session)):
    query = select(StudentEntity).where(StudentEntity.id == id).with_for_update()   # 这里使用with_for_update()，确保在更新时数据不会被其他事务修改
    # 由于只会有一条记录所以这里使用scalar()，而不是scalars()
    record = db_session.execute(query).scalar()
    if not record:
        raise HTTPException(status_code=404, detail=f"Student中id为{id}的记录不存在!")

    # 修改数据方式一：
    # record.name = student.name
    # record.gender = student.gender
    # # 这里要注意，没有调用类似update()而是直接修改返回的数据进行提交即可！！！
    # db_session.commit()
    # return  record

    # 修改数据方式二：
    # 方式一中进行一个一个的更新然后提交，如果字段比较多的话写起来麻烦
    # 注意：这里有一个问题这种更新的方式，前面被查出来的数据并没有被更新，此时直接返回这个数据返回的还是更新前的信息
    # update_query = update(StudentEntity).values(student.model_dump()).where(StudentEntity.id == id)
    # db_session.execute(update_query)
    # db_session.commit()
    # 为把新数据返回，这里需要把id也返回
    # out = StudentOut(**(student.model_dump() | {"id":id}))
    # 其实这里这样子也可能不好，因为我们要更新的字段并不是所有的字段
    # return student.model_dump() | {"id":id}
    # return {"result": student.model_dump(), "id": id}

    # 要处理这个返回的问题，我们可以在外部写一个函数来处理,通过调用这个函数把从数据库查出来对象（在内存中）的对应属性值修改掉
    set_attrs(record,student.model_dump())  # 完成属性值的修改
    db_session.commit() # 提交修改

    return record

def check_student_exists(id:int,db_session: Session,type: Optional[str] = "dml"):
    """
    检查指定id的数据在数据库中是否存在
    :param id: id
    :param db_session: 数据库会话session
    :param type: 操作类型,dml表示需要with_for_update(),其它情况不需要
    :return: 数据库中查询的数据，如果查不到会抛异常404
    """
    if type == "dml":
        # with_for_update()会对查询到的记录加锁，直到提交或回滚事务（session.commit()/session.rollback()）
        query = select(StudentEntity).where(StudentEntity.id == id).with_for_update()   # 这里使用with_for_update()，确保在更新时数据不会被其他事务修改
    else:
        query = select(StudentEntity).where(StudentEntity.id == id)

    # 由于只会有一条记录所以这里使用scalar()，而不是scalars()
    record = db_session.execute(query).scalar()
    if not record:
        raise HTTPException(status_code=404, detail=f"Student中id为{id}的记录不存在!")

    return record

@app.delete("/student/{id}",tags=["学生管理"],summary="删除学生信息")
async def delete_student(id: int,db_session: Session = Depends(get_db_session)):
    exists_student = check_student_exists(id,db_session)

    # 删除数据
    db_session.delete(exists_student)
    db_session.commit()

    return {"message": "删除成功"}



if __name__ == '__main__':
    uvicorn.run("main:app", host="127.0.0.1", port=8012, reload=True)